#ifndef pulseaudio_hpp
#define pulseaudio_hpp

#include "libwifiparty/trackreceiver.hpp"
#include "party-protocol/app.h"
#include "party-protocol/channel.hpp"
#include "party-protocol/packet.hpp"
#include "party-protocol/receiver.hpp"
#include "libwifiparty/mixer.hpp"
#include <asm-generic/socket.h>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <pulse/context.h>
#include <pulse/def.h>
#include <pulse/error.h>
#include <pulse/mainloop-api.h>
#include <pulse/mainloop.h>
#include <pulse/pulseaudio.h>
#include <pulse/sample.h>
#include <pulse/stream.h>
#include <ratio>
#include <sys/socket.h>
#include <chrono>
#include <unistd.h>
#include <fstream>
#include <unistd.h>
#include <signal.h>

typedef std::chrono::duration<int, std::milli> ms;

enum channel_ids
{
    music = 1,
    vocal = 2,
    sync_channel = 3,
};

enum errors
{
    pa_connection_failed,
    pa_stream_creation_failed,
    connect_playback_failed,
    connect_record_failed,
    length_does_not_equal_to_readable,
    met_a_hole,
    stream_drop_failed,
    pa_stream_peek_failed
};
struct coreinfo
{
    Mixer<Trackreceiver> *trackMixer;
    Trackreceiver *musicTrack, *vocalTrack;
    Channel *vocalChannel, *musicChannel;
    Receiver *receiver;

    bool connected;
    pa_stream *playstream, *recordstream, *appaudiostream;
    pa_context *context;
    pa_sample_spec *format;
};
struct receiveInfo
{
    std::chrono::high_resolution_clock::time_point last_receive_time;
    unsigned int last_receive_index;
    receiveInfo()
    {
        this->last_receive_index = 0;
        this->last_receive_time = std::chrono::high_resolution_clock::now();
    }
    void hit(Packet *p)
    {
        std::chrono::high_resolution_clock::time_point this_receive_time = std::chrono::high_resolution_clock::now();
        std::chrono::high_resolution_clock::duration time_elapsed = this_receive_time - last_receive_time;
        ms ms_elapsed = std::chrono::duration_cast<ms>(time_elapsed);
        int ms_elapsed_int = ms_elapsed.count();
        printf("+%dms Receive packet #%d ", ms_elapsed_int, p->header->index);
        if (p->header->index - last_receive_index != 1)
        {
            printf("!");
        }
        last_receive_index = p->header->index;
        last_receive_time = this_receive_time;
        printf("\n");
    }
};

void pa_connect_callback(pa_context *context, void *userdata);
void sink_info_callback(pa_context *context, const pa_sink_info *info, int eol, void *userdata);
static void playcallback(pa_stream *stream, size_t length, void *userdata);
static void recordcallback(pa_stream *stream, size_t length, void *userdata);
static void appAudioCallback(pa_stream *stream, size_t length, void *userdata);
void cleanUp(int sigNum);

std::ofstream debugstream;


coreinfo* painitialize(coreinfo *userdata)
{
    // Create mainloop API & connection to server
    pa_mainloop *mainloop = pa_mainloop_new();
    pa_mainloop_api *mainloopapi = pa_mainloop_get_api(mainloop);
    pa_context *context = pa_context_new(mainloopapi, "Wi-Fi Party");
    bool connected = false;


    pa_context_connect(context, NULL, (pa_context_flags_t)0, NULL);
    pa_context_set_state_callback(context, pa_connect_callback, userdata); //==>void pa_connect_callback
    while (connected == false)
    {
        pa_mainloop_iterate(mainloop, 1, NULL);
    };

    signal(SIGINT, cleanUp);

    pa_mainloop_run(mainloop, NULL);
    return userdata;
}

void pa_connect_callback(pa_context *context, void *userdata)
{
    coreinfo *ci = (coreinfo *)userdata;
    pa_context_state_t state = pa_context_get_state(context);
    switch (state)
    {
    case PA_CONTEXT_UNCONNECTED:
        return;
    case PA_CONTEXT_CONNECTING:
        return;
    case PA_CONTEXT_AUTHORIZING:
        return;
    case PA_CONTEXT_SETTING_NAME:
        return;
    case PA_CONTEXT_TERMINATED:
        return;
    case PA_CONTEXT_FAILED:
        throw pa_connection_failed;
    case PA_CONTEXT_READY:
        ci->connected = true;
        ci->context=context;
        break;
    }
    // Connected
    printf("Connected to pulse audio\n");
    debugstream.open("play.pcm",std::ios::binary|std::ios::out);

    // Set up format
    ci->format=new pa_sample_spec;
    ci->format->rate = 44100;
    ci->format->channels = 1;
    ci->format->format = PA_SAMPLE_S16BE;
    if (!pa_sample_spec_valid(ci->format))
    {
        throw "Invalid format spec";
    }

    // Play stream
    ci->playstream = pa_stream_new(context, "playback", ci->format, NULL);
    if (!ci->playstream)
    {
        throw pa_stream_creation_failed;
    }
    pa_stream_set_write_callback(ci->playstream, playcallback, ci);
    pa_buffer_attr play_buffer_attr = {(uint32_t)-1};
    play_buffer_attr.tlength = pa_usec_to_bytes((pa_usec_t)20000, ci->format); // 15000 us, that is, 0.015s
    int ret = pa_stream_connect_playback(ci->playstream, NULL, &play_buffer_attr, PA_STREAM_ADJUST_LATENCY, NULL, NULL);
    if (ret < 0)
    {
        printf("connect_playback_failed\n");
        printf("errormsg: %s\n", pa_strerror(pa_context_errno(context)));
        throw connect_playback_failed;
    }
}

void sink_info_callback(pa_context *context, const pa_sink_info *sink, int eol, void *userdata){
    printf("Entered sink info callback\n");
    if(eol>0){
        // End of list
        return;
    }
    coreinfo *ci = (coreinfo *)userdata;

    if(sink->state==PA_SINK_RUNNING){
        ci->appaudiostream = pa_stream_new(context, "appaudio", ci->format, NULL);
        if (!ci->appaudiostream)
        {
            throw pa_stream_creation_failed;
        }
        pa_stream_set_read_callback(ci->appaudiostream, appAudioCallback, ci);
        pa_buffer_attr record_buffer_attr = {(uint32_t)-1};
        record_buffer_attr.fragsize = pa_usec_to_bytes((pa_usec_t)20000, ci->format);
        printf("Monitor source name is %s\n", sink->monitor_source_name);
        int ret = pa_stream_connect_record(ci->appaudiostream, sink->monitor_source_name, &record_buffer_attr, PA_STREAM_ADJUST_LATENCY);
        if (ret < 0)
        {
            printf("connect_record_failed\n");
            printf("errormsg: %s\n", pa_strerror(pa_context_errno(context)));
            // printf("errormsg: %s\n", pa_strerror(ret));
            throw connect_record_failed;
        }
    }

}

void startSinging(coreinfo *ci){
    ci->recordstream = pa_stream_new(ci->context, "record", ci->format, NULL);
    if (!ci->recordstream)
    {
        throw pa_stream_creation_failed;
    }
    pa_stream_set_read_callback(ci->recordstream, recordcallback, ci);

    pa_buffer_attr record_buffer_attr = {(uint32_t)-1};
    record_buffer_attr.fragsize = pa_usec_to_bytes((pa_usec_t)20000, ci->format);
    int ret=pa_stream_connect_record(ci->recordstream, NULL, &record_buffer_attr, PA_STREAM_ADJUST_LATENCY);
    if (ret < 0){
        printf("connect_record_failed\n");
        printf("errormsg: %s\n", pa_strerror(pa_context_errno(ci->context)));
        throw connect_record_failed;
    }
}

void stopSinging(coreinfo *ci){
    pa_stream_disconnect(ci->recordstream);
    ci->recordstream=nullptr;
}

void startAppAudio(coreinfo *ci){
    pa_context_get_sink_info_list(ci->context, sink_info_callback, ci);
}

void stopAppAudio(coreinfo *ci){
    pa_stream_disconnect(ci->appaudiostream);
    ci->appaudiostream=nullptr;
}


static void playcallback(pa_stream *stream, size_t length, void *userdata)
{
    char *tmpbuffer = new char[length];
    coreinfo *ci = (coreinfo *)userdata;
    ci->trackMixer->readSound(tmpbuffer, length);
    debugstream.write(tmpbuffer,length);
    pa_stream_write(stream, tmpbuffer, length, NULL, 0LL, PA_SEEK_RELATIVE);
}

static void recordcallback(pa_stream *stream, size_t length, void *userdata)
{
    coreinfo *ci = (coreinfo *)userdata;
    const void *read_source;
    size_t readable;
    pa_stream_peek(stream, &read_source, &readable);
    if (length != readable)
    {
        throw length_does_not_equal_to_readable;
    }
    else if (read_source == NULL)
    {
        throw met_a_hole;
    }
    ci->vocalChannel->send(read_source, readable, 10);
    int ret = pa_stream_drop(stream);
    if (ret != 0)
    {
        throw stream_drop_failed;
    }
}


static void appAudioCallback(pa_stream *stream, size_t length, void *userdata)
{
    // printf("App audio callback length=%ld",length);
    if (length < 100){
        // printf("\n");
        return;
    }
    char send_buffer[length];
    int send_buffer_write=0;
    coreinfo *ci = (coreinfo *)userdata;
    const void *read_source;
    size_t readable;
    int ret;
    while(length>0){
        ret = pa_stream_peek(stream, &read_source, &readable);
        if (ret < 0)
        {
            printf("pa_stream_peek() failed: %s\n", pa_strerror(pa_context_errno(ci->context)));
            throw pa_stream_peek_failed;
        }
        if (read_source != NULL)
        {
            // printf(", read %ld bytes",readable);
            memcpy(send_buffer + send_buffer_write, read_source, readable);
            send_buffer_write += readable;
            length -= readable;
        }else{
            // printf(", hole");
        }
        ret = pa_stream_drop(stream);
        if (ret != 0)
        {
            throw stream_drop_failed;
        }
    }
    // printf("\n");

    ci->musicChannel->send(send_buffer, send_buffer_write, 0);
}

void cleanUp(int signum)
{
    // delete receiver;
    // close(s);
    exit(0);
}

#endif
